第14章 智能推荐系统¶

智能推荐系统属于非监督式学习, 是机器学习一个非常重要的应用领域, 它能带来的经济价值往往是直接且非常可观的

14.1 智能推荐系统的基本原理¶

14.1.1 智能推荐系统的应用场景¶

互联网每天都在产生海量信息,用户行为数据呈现爆发式增长用户会有直接和明确的需求,但也可能是在漫无目的地搜寻

智能推荐系统可以通过分析用户的浏览次数、浏览时间、点击率等指标,挖掘出用户感兴趣的内容或商品,然后进行个性化推荐

如果推荐的内容或商品能高效匹配用户的需求,就能优化用户体验,提高用户黏性,创造额外收入

14.1.2 智能推荐系统的基础: 协同过滤算法¶

搭建智能推荐系统的算法有很多, 商业实战中用得较多的是协同过滤算法(collaborative filtering)

协同过滤算法的原理是根据用户群体的产品偏好数据, 发现用户与物品之间的相关性, 并基于这些相关性为用户进行推荐

根据原理的不同, 协同过滤算法分为两类: 基于用户的协同过滤算法和基于物品的协同过滤算法

  1. 基于用户的协同过滤算法

    基于用户的协同过滤算法的本质是寻找相似的用户: 通过一个用户的相关数据寻找与该用户相似的其他用户, 进而为该用户推荐相似用户关注的产品

    比如用户1和用户2都给商品A,B,C打了高分, 那么可以将用户1和用户2划分在同一个用户群体, 此时若用户2还给商品D打了高分, 那么就可以将商品D推荐给用户1

    image.png

  1. 基于物品的协同过滤算法

    基于物品的协同过滤算法的本质是寻找相似的物品: 通过一个物品的相关数据取寻找与该物品相似的其他物品, 进而为关注该物品的用户推荐相似的物品

    比如图书A和图书B都被用户1,2,3购买过, 那么可以认为图书A和图书B具有较强的相似度, 进而推测喜欢图书A的用户同样也会喜欢图书B, 当用户4购买图书B时, 根据图书A和图书B的相似性, 可以将图书A推荐给用户4

    image.png

在商业实战中, 大多数应用场景偏向于使用基于物品的协同过滤算法, 主要原因有:

  • 通常情况下, 用户的数量是非常庞大的, 而物品的数量则相对有限, 因此, 计算不同物品的相似度往往比计算不同的用户的相似度容易很多

  • 用户的喜好较为多变, 而物品的属性较明确, 不随时间变化, 过去的用户对物品的评分长期有效, 所以物品的相似度比较固定, 可以预先离线计算好物品的相似度, 把结果存在表中, 需要向用户进行推荐时再从表中调用

14.2 计算相似度的常用方法¶

无论是基于用户还是基于产品的协同过滤算法, 其本质都是寻找数据之间的相似度, 计算相似度的三种常用方法--欧氏距离、余弦值和皮尔逊系数

表中为3个用户对3个物品的评分, 数字代表星级:

image.png

因为评分数据都在0-5之间, 量级一致, 所以无须做标准化处理, 如果数据的量级存在较大差异, 应先做标准化处理

14.2.1 欧氏距离¶

欧式距离的计算之前已经说过, 对空间坐标 $A$ 和 $B$ , $A$ 和 $B$ 间的欧氏距离计算公式为:

$$ d(A,B) = ||\overrightarrow{AB}||_2 $$

将前面的用户评分表中的数据带入, 计算物品A和物品B的欧氏距离为:

$$ d(A,B) = \sqrt{(5-4)^2 + (1-2)^2 + (5-2)^2} = \sqrt{11} = 3.32 $$

除了直接比较欧氏距离, 还可以利用欧氏距离衍生出的相似度公式来衡量两者的相似度, 基于欧氏距离的相似度 $sim(A,B)$ 定义为:

$$ sim(A,B) = \frac{1}{1 + d(A,B)} $$

将前面计算出的欧式距离代入, 可以计算出物品A和物品B的相似度为:

$$ sim(A,B) = \frac{1}{1 + 3.32} = 0.23 $$

对前面的用户评分表的计算结果为:

image.png

在第7章K近邻算法中也使用了欧式距离来衡量不同样本间的距离, 只不过那里是监督式学习, 这里则是非监督式学习

In [1]:
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df
Out[1]:
用户1 用户2 用户3
物品A 5 1 5
物品B 4 2 2
物品C 4 2 1
In [2]:
import numpy as np
dist = np.linalg.norm(df.iloc[0] - df.iloc[1]) # numpy库可以方便地计算两个向量的欧氏距离

print('%.3f' % dist,)
print('{:.3f}'.format(dist))
print(np.around(dist, decimals=3))
3.317
3.317
3.317

14.2.2 余弦相似度¶

在向量空间中, 向量 $\overrightarrow{a}$ 和向量 $\overrightarrow{b}$ 的夹角的余弦值可以表示为:

$$ cos<\overrightarrow{a},\overrightarrow{b}> = \frac{\overrightarrow{a}\cdot \overrightarrow{b}}{|\overrightarrow{a}||\overrightarrow{b}|} = \frac{\overrightarrow{a}\cdot \overrightarrow{b}}{||\overrightarrow{a}||_2||\overrightarrow{b}||_2} $$

  • $||x||_p$, $x$ 的 $Lp$ 范数, 当 $p$ 取 $n$ 时, 称为向量 $x$ 的 $Ln-norm$, 计算公式为:

    $$ ||x||_p := \sqrt[p]{\sum_{i=0}^{n}|x_i|^p} $$

可以直接用Scikit-Learn库中的cosine_similarity()函数计算各物品之间的余弦相似度, 并整理成DataFrame格式的二维表格:

In [3]:
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df
Out[3]:
用户1 用户2 用户3
物品A 5 1 5
物品B 4 2 2
物品C 4 2 1
In [4]:
from sklearn.metrics.pairwise import cosine_similarity
user_similarity = cosine_similarity(df)

user_similarity = np.around(user_similarity, decimals=3) # 保留三位小数

pd.DataFrame(user_similarity, columns=['物品A', '物品B', '物品C'], index=['物品A', '物品B', '物品C'])
Out[4]:
物品A 物品B 物品C
物品A 1.000 0.915 0.825
物品B 0.915 1.000 0.980
物品C 0.825 0.980 1.000

14.2.3 皮尔逊相关系数¶

(皮尔逊)相关系数 $r$ 时用于描述两个变量间相关强弱程度的统计量, 取值范围为 $[-1,1]$, 为正值代表两个变量存在正相关关系, 为负值代表两个变量存在负相关关系, 其绝对值越大, 说明相关性越强, 计算公式为:

$$ r = corr(X,Y) = \frac{Cov(X,Y)}{\sqrt{D(X)}\sqrt{D(Y)}} = \frac{Cov(X,Y)}{\sqrt{\sigma_X^2}\sqrt{\sigma_Y^2}} = \frac{\sigma_{XY}}{\sigma_X\sigma_Y} $$

In [5]:
from scipy.stats import pearsonr
X = [1, 3, 5, 7, 9]
Y = [9, 8, 6, 4, 2]
corr = pearsonr(X, Y)
print('相关系数r值为 %.3f, 显著性水平P值为 %.3f' % (corr[0],corr[1]))
相关系数r值为 -0.994, 显著性水平P值为 0.001
In [6]:
import pandas as pd
df = pd.DataFrame([[5, 1, 5], [4, 2, 2], [4, 2, 1]], columns=['用户1', '用户2', '用户3'], index=['物品A', '物品B', '物品C'])
df = df.T

df
Out[6]:
物品A 物品B 物品C
用户1 5 4 4
用户2 1 2 2
用户3 5 2 1
In [7]:
# 物品A与其他物品的皮尔逊相关系数
A = df['物品A']
corr_A = df.corrwith(A)
corr_A
Out[7]:
物品A    1.000000
物品B    0.500000
物品C    0.188982
dtype: float64
In [8]:
# 皮尔逊系数表,获取各物品相关性
df.corr()
Out[8]:
物品A 物品B 物品C
物品A 1.000000 0.500000 0.188982
物品B 0.500000 1.000000 0.944911
物品C 0.188982 0.944911 1.000000

14.3 案例实战: 电影智能推荐系统¶

14.3.1 案例背景¶

如果视频平台能利用基于物品的智能推荐系统,从用户对观看过的电影给出的评分中有效地挖掘数据,便能根据用户的偏好个性化地推荐更多类似的电影,从而优化用户体验,提高用户黏性,创造额外收入

14.3.2 数据读取与处理¶

In [9]:
import pandas as pd 
movies = pd.read_excel('电影.xlsx')
movies.head()
Out[9]:
电影编号 名称 类别
0 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想
1 2 勇敢者的游戏(1995) 冒险|儿童|幻想
2 3 斗气老顽童2(1995) 喜剧|爱情
3 4 待到梦醒时分(1995) 喜剧|剧情|爱情
4 5 新娘之父2(1995) 喜剧
In [10]:
score = pd.read_excel('评分.xlsx')
score.head()
Out[10]:
用户编号 电影编号 评分
0 1 1 4.0
1 1 3 4.0
2 1 6 4.0
3 1 47 5.0
4 1 50 5.0
In [11]:
df = pd.merge(movies, score, on='电影编号')
df.head()
Out[11]:
电影编号 名称 类别 用户编号 评分
0 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想 1 4.0
1 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想 5 4.0
2 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想 7 4.5
3 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想 15 2.5
4 1 玩具总动员(1995) 冒险|动画|儿童|喜剧|幻想 17 4.5
In [12]:
df.to_excel('电影推荐系统.xlsx')
In [13]:
df['评分'].value_counts()  # 查看各个评分的出现的次数
Out[13]:
评分
4.0    26794
3.0    20017
5.0    13180
3.5    13129
4.5     8544
2.0     7545
2.5     5544
1.0     2808
1.5     1791
0.5     1369
Name: count, dtype: int64
In [14]:
import matplotlib.pyplot as plt
df['评分'].hist(bins=20)  # hist()函数绘制直方图,竖轴为各评分出现的次数
Out[14]:
<Axes: >
No description has been provided for this image
In [15]:
ratings = pd.DataFrame(df.groupby('名称')['评分'].mean())
ratings.sort_values('评分', ascending=False).head()
Out[15]:
评分
名称
假小子(1997) 5.0
福尔摩斯和华生医生历险记:讹诈之王(1980) 5.0
机器人(2016) 5.0
奥斯卡(1967) 5.0
人类状况III(1961) 5.0
In [16]:
ratings['评分次数'] = df.groupby('名称')['评分'].count()
ratings.sort_values('评分次数', ascending=False).head()
Out[16]:
评分 评分次数
名称
阿甘正传(1994) 4.164134 329
肖申克的救赎(1994) 4.429022 317
低俗小说(1994) 4.197068 307
沉默的羔羊(1991) 4.161290 279
黑客帝国(1999) 4.192446 278
In [17]:
user_movie = df.pivot_table(index='用户编号', columns='名称', values='评分')
user_movie.tail()
Out[17]:
名称 007之黄金眼(1995) 100个女孩(2000) 100条街道(2016) 101忠狗续集:伦敦大冒险(2003) 101忠狗(1961) 101雷克雅未克(2000) 102只斑点狗(2000) 10件或更少(2006) 10(1979) 11:14(2003) ... 龙珠:神秘冒险(1988) 龙珠:血红宝石的诅咒(1986) 龙珠:魔鬼城堡中的睡公主(1987) 龙种子(1944) 龙纹身的女孩(2011) 龙舌兰日出(1988) 龙虾(2015) 龙:夜之怒的礼物(2011) 龙:李小龙的故事(1993) 龟日记(1985)
用户编号
606 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
607 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
608 4.0 NaN NaN NaN NaN NaN NaN 3.5 NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
609 4.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
610 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN ... NaN NaN NaN NaN 4.0 NaN 4.5 NaN NaN NaN

5 rows × 9687 columns

In [18]:
user_movie.describe()  # 因为数据量较大,这个耗时可能会有1分钟左右
Out[18]:
名称 007之黄金眼(1995) 100个女孩(2000) 100条街道(2016) 101忠狗续集:伦敦大冒险(2003) 101忠狗(1961) 101雷克雅未克(2000) 102只斑点狗(2000) 10件或更少(2006) 10(1979) 11:14(2003) ... 龙珠:神秘冒险(1988) 龙珠:血红宝石的诅咒(1986) 龙珠:魔鬼城堡中的睡公主(1987) 龙种子(1944) 龙纹身的女孩(2011) 龙舌兰日出(1988) 龙虾(2015) 龙:夜之怒的礼物(2011) 龙:李小龙的故事(1993) 龟日记(1985)
count 132.000000 4.00 1.0 1.0 44.000000 1.0 9.000000 3.000000 4.000000 4.00 ... 1.0 1.0 2.000000 1.0 42.000000 13.000000 7.000000 1.0 8.00000 2.0
mean 3.496212 3.25 2.5 2.5 3.431818 3.5 2.777778 2.666667 3.375000 3.75 ... 3.5 3.5 3.250000 3.5 3.488095 3.038462 4.000000 5.0 2.81250 4.0
std 0.859381 0.50 NaN NaN 0.751672 NaN 0.833333 1.040833 1.030776 0.50 ... NaN NaN 0.353553 NaN 1.327422 0.431158 0.707107 NaN 1.03294 0.0
min 0.500000 2.50 2.5 2.5 1.500000 3.5 2.000000 1.500000 2.000000 3.00 ... 3.5 3.5 3.000000 3.5 0.500000 2.000000 3.000000 5.0 0.50000 4.0
25% 3.000000 3.25 2.5 2.5 3.000000 3.5 2.000000 2.250000 3.125000 3.75 ... 3.5 3.5 3.125000 3.5 2.625000 3.000000 3.500000 5.0 2.87500 4.0
50% 3.500000 3.50 2.5 2.5 3.500000 3.5 2.500000 3.000000 3.500000 4.00 ... 3.5 3.5 3.250000 3.5 4.000000 3.000000 4.000000 5.0 3.00000 4.0
75% 4.000000 3.50 2.5 2.5 4.000000 3.5 3.000000 3.250000 3.750000 4.00 ... 3.5 3.5 3.375000 3.5 4.000000 3.000000 4.500000 5.0 3.12500 4.0
max 5.000000 3.50 2.5 2.5 5.000000 3.5 4.500000 3.500000 4.500000 4.00 ... 3.5 3.5 3.500000 3.5 5.000000 4.000000 5.000000 5.0 4.00000 4.0

8 rows × 9687 columns

In [19]:
import warnings

warnings.filterwarnings('ignore')
In [20]:
FG = user_movie['阿甘正传(1994)']  # FG是Forrest Gump(),阿甘英文名称的缩写
pd.DataFrame(FG).head()
Out[20]:
阿甘正传(1994)
用户编号
1 4.0
2 NaN
3 NaN
4 NaN
5 NaN
In [21]:
# axis默认为0,计算user_movie各列与FG的相关系数
corr_FG = user_movie.corrwith(FG)
similarity = pd.DataFrame(corr_FG, columns=['相关系数'])
similarity.head()
Out[21]:
相关系数
名称
007之黄金眼(1995) 0.217441
100个女孩(2000) NaN
100条街道(2016) NaN
101忠狗续集:伦敦大冒险(2003) NaN
101忠狗(1961) 0.141023
In [22]:
similarity.dropna(inplace=True)  # 或写成similarity=similarity.dropna()
similarity.head()
Out[22]:
相关系数
名称
007之黄金眼(1995) 0.217441
101忠狗(1961) 0.141023
102只斑点狗(2000) -0.857589
10件或更少(2006) -1.000000
11:14(2003) 0.500000
In [23]:
similarity_new = pd.merge(similarity, ratings['评分次数'], left_index=True, right_index=True)
similarity_new.head()
Out[23]:
相关系数 评分次数
名称
007之黄金眼(1995) 0.217441 132
101忠狗(1961) 0.141023 44
102只斑点狗(2000) -0.857589 9
10件或更少(2006) -1.000000 3
11:14(2003) 0.500000 4
In [24]:
# 第二种合并方式
similarity_new = similarity.join(ratings['评分次数'])
similarity_new.head()
Out[24]:
相关系数 评分次数
名称
007之黄金眼(1995) 0.217441 132
101忠狗(1961) 0.141023 44
102只斑点狗(2000) -0.857589 9
10件或更少(2006) -1.000000 3
11:14(2003) 0.500000 4
In [25]:
similarity_new[similarity_new['评分次数'] > 20].sort_values(by='相关系数', ascending=False).head()  # 选取阈值
Out[25]:
相关系数 评分次数
名称
阿甘正传(1994) 1.000000 329
抓狂双宝(1996) 0.723238 31
雷神:黑暗世界(2013) 0.715809 21
致命吸引力(1987) 0.701856 36
X战警:未来的日子(2014) 0.682284 30

补充知识点: pandas库的分类函数groupby()函数¶

In [26]:
import pandas as pd
data = pd.DataFrame([['战狼2', '丁一', 6, 8], ['攀登者', '王二', 8, 6], ['攀登者', '张三', 10, 8], ['卧虎藏龙', '李四', 8, 8], ['卧虎藏龙', '赵五', 8, 10]], columns=['电影名称', '影评师', '观前评分', '观后评分'])
In [27]:
data
Out[27]:
电影名称 影评师 观前评分 观后评分
0 战狼2 丁一 6 8
1 攀登者 王二 8 6
2 攀登者 张三 10 8
3 卧虎藏龙 李四 8 8
4 卧虎藏龙 赵五 8 10
In [28]:
means = data.groupby('电影名称')[['观后评分']].mean()
means
Out[28]:
观后评分
电影名称
卧虎藏龙 9.0
战狼2 8.0
攀登者 7.0
In [29]:
means = data.groupby('电影名称')[['观前评分', '观后评分']].mean()
means
Out[29]:
观前评分 观后评分
电影名称
卧虎藏龙 8.0 9.0
战狼2 6.0 8.0
攀登者 9.0 7.0
In [30]:
means = data.groupby(['电影名称', '影评师'])[['观后评分']].mean()
means
Out[30]:
观后评分
电影名称 影评师
卧虎藏龙 李四 8.0
赵五 10.0
战狼2 丁一 8.0
攀登者 张三 8.0
王二 6.0
In [31]:
count = data.groupby('电影名称')[['观后评分']].count()
count
Out[31]:
观后评分
电影名称
卧虎藏龙 2
战狼2 1
攀登者 2
In [32]:
count = count.rename(columns={'观后评分':'评分次数'})
count
Out[32]:
评分次数
电影名称
卧虎藏龙 2
战狼2 1
攀登者 2